<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>SSPS - Chadams Studios (slightly upgraded)</title>
  <script src="../dist/gpu-browser.min.js"></script>
  <script>
    function XSSPS(particleCount, renderSize, onAfterInput) {
      this.dt = 0;
      this.renderSize = renderSize || 512;
      this.particleCount = particleCount || 512;
      this.onAfterInput = onAfterInput;

      this.hashLength = 128;
      this.hashWSize = 3.0;
      this.hashBinLen = 128;
      this.rayCount = 128;
      this.rayDist = 20;
      this.rfScale = 2.0;

      this.restDensity = 0.1;
      this.fieldLen = 4;
      this.gConst = 0.02;
      this.bound = 8;

      this.shootIndex = 0;
      this.keys = {};

      this.canvas = document.createElement('canvas');
      const gpu = this.gpu = new GPU({
        canvas: this.canvas,
        mode: 'gpu'
      });

      /*
          RENDER SUPPORT FUNCTIONS (GLSL)
       */

      gpu.addNativeFunction('raySphere',
        `vec2 raySphere(vec3 r0, vec3 rd, vec3 s0, float sr) {
          float a = dot(rd, rd);
          vec3 s0_r0 = r0 - s0;
          float b = 2.0 * dot(rd, s0_r0);
          float c = dot(s0_r0, s0_r0) - (sr * sr);
          float test = b*b - 4.0*a*c;
          if (test < 0.0) {
              return vec2(-1.0, 0.);
          }
          test = sqrt(test);
          float X = (-b - test)/(2.0*a);
          float Y = (-b + test)/(2.0*a);
          return vec2(
              X,
              Y-X
          );
        }`
      );

      gpu.addNativeFunction('getRay0',
        `vec3 getRay0(vec2 uv,  vec3 c0, vec3 cd, vec3 up, float s0, float s1, float slen) {
          up = normalize(cross(cross(cd, up), cd));

          vec3 vup = normalize(cross(cd, up));
          vec3 vleft = up;

          vec2 X = (uv - vec2(0.5, 0.5)) * s0;

          return c0 + vup * X.y + vleft * X.x;
        }`
      );

      gpu.addNativeFunction('getRayDir',
        `vec3 getRayDir(vec3 R0,  vec2 uv,  vec3 c0, vec3 cd, vec3 up, float s0, float s1, float slen) {
          up = normalize(cross(cross(cd, up), cd));

          vec3 vup = normalize(cross(cd, up));
          vec3 vleft = up;

          vec2 X = (uv - vec2(0.5, 0.5)) * s1;

          vec3 far = vup * X.y + vleft * X.x;

          return normalize((normalize(cd) * slen + far + c0) - R0);
        }`
      );

      gpu.addNativeFunction('reflectWrap',
        `vec3 reflectWrap(vec3 I, vec3 N) {
          return normalize(reflect(normalize(I), normalize(N)));

        }`
      );

      gpu.addNativeFunction('refractWrap',
        `vec3 refractWrap(vec3 I, vec3 N, float eta) {
          return normalize(refract(normalize(I), normalize(N), eta));

        }`
      );

      gpu.addNativeFunction('distanceWrap',
        `float distanceWrap(vec3 A, vec3 B) {
          return length(A - B);
        }`
      );

      /*
          UPDATE SUPPORT FUNCTIONS (GLSL)
       */

      gpu.addNativeFunction('compGravity',
        `vec3 compGravity(vec3 M, vec3 J, float jMass, float f) {
          vec3 delta = J - M;
          float dlen = length(delta);

          if (dlen > 0.05) {
              float dlen2 = jMass / (max(dlen*dlen, 1.) * 0.1);
              return dlen2 * (delta / dlen) * f;
          }
          return vec3(0., 0., 0.);
        }`
      );

      gpu.addNativeFunction('partDensity',
        `vec2 partDensity(vec3 M, vec3 J, float fLen) {
          vec3 delta = J - M;
          float len = length(delta);

          if (len < fLen) {
              float t = 1. - (len / fLen);
              return vec2(t*t, t*t*t);
          }
          return vec2(0., 0.);
        }`
      );

      gpu.addNativeFunction('pressForce',
        `vec3 pressForce(vec3 M, vec3 J, vec3 Mv, vec3 Jv, float fLen, float dt, float spressure, float snpressure, float viscdt) {
          vec3 delta = J - M;
          float len = length(delta);
          if (len < fLen) {
              float t = 1. - (len / fLen);
              delta *= dt * t * (spressure + snpressure * t) / (2. * len);
              vec3 deltaV = Jv - Mv;
              deltaV *= dt * t * viscdt;
              return -(delta - deltaV);
          }
          return vec3(0., 0., 0.);
        }`
      );

      /*
          INIT SUPPORT FUNCTIONS
       */

      gpu.addNativeFunction('random',
        `float random(float sequence, float seed) {
          return fract(sin(dot(vec2(seed, sequence), vec2(12.9898, 78.233))) * 43758.5453);
        }`
      );

      /*
          VELOCITY UPDATE KERNEL
       */

      this.updateVelocity = gpu.createKernel(function(
        positions,
        velocities,
        attrs,
        pullMass,
        camP,
        camDir,
        moveTPullR,
        oDensity,
        oVisc,
        oMass,
        oIncomp,
        dt
      ) {
        var playerPos = [
          camP[0] + camDir[0] * moveTPullR,
          camP[1] + camDir[1] * moveTPullR,
          camP[2] + camDir[2] * moveTPullR,
        ];
        // Unpack particle
        var comp = this.thread.x % 3;
        var me = (this.thread.x - comp) / 3;
        var mx = positions[me*3],
          my = positions[me*3+1],
          mz = positions[me*3+2];
        var mpos = [mx, my, mz];
        var mvx = velocities[me*3],
          mvy = velocities[me*3+1],
          mvz = velocities[me*3+2];
        var mvel = [mvx, mvy, mvz];
        var mmass = oMass;
        var incomp = oIncomp;
        var viscdt = oVisc;
        var ddensity = 0.0,
          nddensity = 0.0;

        // Compute pressure on this particle
        for (var i=0; i<this.constants.particleCount; i++) {
          var opos = [positions[i*3], positions[i*3+1], positions[i*3+2]];
          var density = oDensity;
          var dret = partDensity(mpos, opos, this.constants.fieldLen);
          ddensity += dret[0] * density;
          nddensity += dret[1] * density;
        }

        // Interact with player by adding pressure
        var opos = [playerPos[0], playerPos[1], playerPos[2]];
        var fl2 = this.constants.fieldLen;
        var pf = [0, 0];
        if (pullMass > 0.0) {
          pf = partDensity(mpos, opos, fl2);
        }
        ddensity += pf[0] * (pullMass / 10.0 * this.constants.restDensity);
        nddensity += pf[1] * (pullMass / 10.0 * this.constants.restDensity);

        var spressure = (ddensity - this.constants.restDensity) * incomp;
        var snpressure = nddensity * incomp;

        // Compute force from pressure on this particle
        var ret = [mvx, mvy, mvz];

        for (var i=0; i<this.constants.particleCount; i++) {
          if (i - me > 0.01) {
            var opos = [positions[i*3], positions[i*3+1], positions[i*3+2]];
            var ovel = [velocities[i*3], velocities[i*3+1], velocities[i*3+2]];
            var mass = oMass;
            var jmf = (2. * mass) / (mass + mmass);
            var dret = pressForce(mpos, opos, mvel, ovel, this.constants.fieldLen, dt, spressure, snpressure, viscdt);
            ret[0] += dret[0] * jmf; ret[1] += dret[1] * jmf; ret[2] += dret[2] * jmf;
          }
        }

        // Player pressure
        if (pullMass > 0.0) {
          var opos = [playerPos[0], playerPos[1], playerPos[2]];
          var ovel = [0, 0, 0];
          var mass = 1.0;
          var jmf = (2. * mass) / (mass + mmass);
          var dret = pressForce(mpos, opos, mvel, ovel, this.constants.fieldLen, dt, spressure, snpressure, viscdt);
          ret[0] += dret[0] * jmf;
          ret[1] += dret[1] * jmf;
          ret[2] += dret[2] * jmf;
        }

        // Compute gravitational force on this particle
        var gf = this.constants.gConst * dt * 0.1;
        for (var i=0; i<this.constants.particleCount; i++) {
          var opos = [positions[i*3], positions[i*3+1], positions[i*3+2]];
          var mass = attrs[i*6+2];
          var dret = compGravity(mpos, opos, mass, gf);
          ret[0] += dret[0]; ret[1] += dret[1]; ret[2] += dret[2];
        }

        // Enforce scene boundaries
        if (ret[0] < 0 && mpos[0] < -this.constants.bound) {
          ret[0] = -ret[0];
        } else if (ret[0] > 0 && mpos[0] > this.constants.bound) {
          ret[0] = -ret[0];
        }

        if (ret[1] < 0 && mpos[1] < -this.constants.bound) {
          ret[1] = -ret[1];
        } else if (ret[1] > 0 && mpos[1] > this.constants.bound) {
          ret[1] = -ret[1];
        }

        if (ret[2] < 0 && mpos[2] < -this.constants.bound) {
          ret[2] = -ret[2];
        } else if (ret[2] > 0 && mpos[2] > this.constants.bound) {
          ret[2] = -ret[2];
        }

        if (comp < 0.01) {
          return ret[0];
        } else if (comp < 1.01) {
          return ret[1];
        } else if (comp < 2.01) {
          return ret[2];
        }
      }, {
        constants: {
          particleCount: this.particleCount,
          bound: this.bound,
          fieldLen: this.fieldLen,
          gConst: this.gConst,
          restDensity: ((4 / 3) * Math.PI * Math.pow(this.fieldLen, 3)) * this.restDensity
        },
        loopMaxIterations: this.particleCount,
        output: [ this.particleCount*3 ],
        pipeline: true,
        tactic: 'speed',
      });

      /*
          SET POSITION/VELOCITES
       */
      this.setPosition = gpu.createKernel(function(list, index, camP, camDir, amount) {
        if (Math.abs(index - Math.floor(this.thread.x / this.constants.pitch)) < 0.01) {
          const i = this.thread.x % this.constants.pitch;
          return camP[i] + camDir[i] * amount;
        } else {
          return list[this.thread.x];
        }
      }, {
        output: [ this.particleCount * 3 ],
        pipeline: true,
        constants: { particleCount: this.particleCount, pitch: 3 },
        loopMaxIterations: this.particleCount,
        tactic: 'speed',
      });

      this.setVelocity = gpu.createKernel(function(list, index, camDir, amount) {
        if (Math.abs(index - Math.floor(this.thread.x / this.constants.pitch)) < 0.01) {
          return camDir[this.thread.x % this.constants.pitch] * amount;
        } else {
          return list[this.thread.x];
        }
      }, {
        output: [ this.particleCount * 3 ],
        pipeline: true,
        constants: { particleCount: this.particleCount, pitch: 3 },
        loopMaxIterations: this.particleCount,
        tactic: 'speed',
      });

      /*
          POSITION UPDATE KERNEL
       */

      this.updatePosition = gpu.createKernel(function(positions, velocities, dt) {
        return positions[this.thread.x] + velocities[this.thread.x] * dt;
      }, {
        output: [ this.particleCount * 3 ],
        pipeline: true,
        loopMaxIterations: this.particleCount,
        tactic: 'speed',
      });

      this.rotateCamera = gpu.createKernel(function(rotLR, rotUD, camCenter, camDir, camUp, camNearWidth, camFarWidth, camDist) {
        var uv = [0.5+rotLR, 0.5+rotUD];

        var CC = [camCenter[0], camCenter[1], camCenter[2]],
          CD = [camDir[0], camDir[1], camDir[2]],
          CU = [camUp[0], camUp[1], camUp[2]];
        var ray0 = getRay0(uv, CC, CD, CU, camNearWidth, camFarWidth, camDist);
        var rayDir = getRayDir(ray0, uv, CC, CD, CU, camNearWidth, camFarWidth, camDist);

        if (this.thread.x === 0) {
          return rayDir[0];
        } else if (this.thread.x === 1) {
          return rayDir[1];
        } else {
          return rayDir[2];
        }
      }, {
        output: [ 3 ],
        tactic: 'speed',
        pipeline: true
      });

      this.updateCameraPosition = gpu.createKernel(function(camDir, camP, moveTFR, dt) {
        var dlen = Math.sqrt(
          (camDir[0] * camDir[0])
          + (camDir[1] * camDir[1])
          + (camDir[2] * camDir[2])
        );
        return camP[this.thread.x] + (camDir[this.thread.x] / dlen) * moveTFR * dt * 6;
      }, {
        output: [3],
        tactic: 'speed',
        pipeline: true
      });

      /*
          RENDER KERNEL
       */

      this.renderMode = 0;

      this.renderKernel = [
        gpu.createKernel(function(positions, camCenter, camDir, camUp, camNearWidth, camFarWidth, camDist) {

          let uv = [this.thread.x / (this.constants.renderSize-1), this.thread.y / (this.constants.renderSize-1)];

          let CC = [camCenter[0], camCenter[1], camCenter[2]],
            CD = [camDir[0], camDir[1], camDir[2]],
            CU = [camUp[0], camUp[1], camUp[2]];

          let ray0 = getRay0(uv, CC, CD, CU, camNearWidth, camFarWidth, camDist);
          let rayDir = getRayDir(ray0, uv, CC, CD, CU, camNearWidth, camFarWidth, camDist);

          this.color(0., 0., 0., 1.);

          let outClr = [
            0., 0., 0.
          ];

          let ds = 0.0;
          let done = 0.0;
          let norm = [0., 0., 0.];
          for (let i=0; i<this.constants.ICOUNT; i++) {
            if (done < 0.5) {
              let rnow = [
                ray0[0] + rayDir[0] * ds,
                ray0[1] + rayDir[1] * ds,
                ray0[2] + rayDir[2] * ds
              ];

              norm[0] = 0.; norm[1] = 0.; norm[2] = 0.;

              let m = 0.0;
              let fq = 0.0;
              let nt = 0.0;
              let dmin = 1000.0;
              for (let j=0; j<this.constants.particleCount; j++) {
                let off = j * 3;
                let dx  = positions[off], dy  = positions[off+1.], dz  = positions[off+2.];
                dx -= rnow[0];        dy -= rnow[1];           dz -= rnow[2];
                let r = Math.sqrt(dx*dx + dy*dy + dz*dz);
                let x = r / this.constants.rfScale;
                if (x < 1.0) {
                  let q = 1.0 - x*x*x*(x*(x*6.0-15.0)+10.0);
                  norm[0] += (dx/r) * q;
                  norm[1] += (dy/r) * q;
                  norm[2] += (dz/r) * q;
                  fq += q;
                  nt += q;
                  m += 1.0;
                }
                else {
                  dmin = Math.min(dmin, r - this.constants.rfScale);
                }
              }
              let dist = dmin + 0.1;

              norm[0] /= nt; norm[1] /= nt; norm[2] /= nt;

              if (m > 0.5) {
                dist = (0.5333 * this.constants.rfScale) * (0.5 - fq);
              }

              ds += dist;
              if (dist < 0.01) {
                done = 1.0;
              }
              if (ds >= this.constants.rayDist) {
                done = 1.0;
              }
            }
          }
          ds = Math.min(ds, this.constants.rayDist);

          let ref = refractWrap(norm, rayDir, 1./1.5);
          let dot = ref[0] * rayDir[0] + ref[1] * rayDir[1] + ref[2] * rayDir[2];
          let int = (1. - Math.pow(ds / this.constants.rayDist, 1.5));
          let light = Math.min(1., Math.max(dot*dot, 0.)) * int;

          outClr[2] = int + light;
          outClr[1] = light;
          outClr[0] = light;

          this.color(Math.min(outClr[0], 1.), Math.min(outClr[1], 1.), Math.min(outClr[2], 1.), 1.);

        }, {
          graphical: true,

          constants: {
            rayDist: this.rayDist,
            rfScale: this.rfScale,
            bound: this.bound,
            renderSize: this.renderSize,
            particleCount: this.particleCount,
            ICOUNT: 48
          },
          loopMaxIterations: 48 * this.particleCount,
          output: [this.renderSize, this.renderSize],
          tactic: 'speed',
        }),
        gpu.createKernel(function(positions, camCenter, camDir, camUp, camNearWidth, camFarWidth, camDist) {
          let uv = [this.thread.x / (this.constants.renderSize-1), this.thread.y / (this.constants.renderSize-1)];

          let CC = [camCenter[0], camCenter[1], camCenter[2]],
            CD = [camDir[0], camDir[1], camDir[2]],
            CU = [camUp[0], camUp[1], camUp[2]];

          let ray0 = getRay0(uv, CC, CD, CU, camNearWidth, camFarWidth, camDist);
          let rayDir = getRayDir(ray0, uv, CC, CD, CU, camNearWidth, camFarWidth, camDist);

          this.color(0., 0., 0., 1.);

          let outClr = [
            0., 0., 0.
          ];

          let ds = 0.0;
          let done = 0.0;
          let norm = [0., 0., 0.];
          for (let i=0; i<this.constants.ICOUNT; i++) {
            if (done < 0.5) {
              let rnow = [
                ray0[0] + rayDir[0] * ds,
                ray0[1] + rayDir[1] * ds,
                ray0[2] + rayDir[2] * ds
              ];

              norm[0] = 0.; norm[1] = 0.; norm[2] = 0.;

              let m = 0.0;
              let fq = 0.0;
              let nt = 0.0;
              let dmin = 1000.0;
              for (let j=0; j<this.constants.particleCount; j++) {
                let off = j * 3;
                let dx  = positions[off], dy  = positions[off+1.], dz  = positions[off+2.];
                dx -= rnow[0];        dy -= rnow[1];           dz -= rnow[2];
                let r = Math.sqrt(dx*dx + dy*dy + dz*dz);
                let x = r / this.constants.rfScale;
                if (x < 1.0) {
                  let q = 1.0 - x*x*x*(x*(x*6.0-15.0)+10.0);
                  norm[0] += (dx/r) * q;
                  norm[1] += (dy/r) * q;
                  norm[2] += (dz/r) * q;
                  fq += q;
                  nt += q;
                  m += 1.0;
                }
                else {
                  dmin = Math.min(dmin, r - this.constants.rfScale);
                }
              }
              let dist = dmin + 0.1;

              norm[0] /= nt; norm[1] /= nt; norm[2] /= nt;

              if (m > 0.5) {
                dist = (0.5333 * this.constants.rfScale) * (0.5 - fq);
              }

              ds += dist;
              if (dist < 0.01) {
                done = 1.0;
              }
              if (ds >= this.constants.rayDist) {
                done = 1.0;
              }
            }
          }
          ds = Math.min(ds, this.constants.rayDist);

          let ref = refractWrap(norm, rayDir, 1./1.5);
          let dot = ref[0] * rayDir[0] + ref[1] * rayDir[1] + ref[2] * rayDir[2];
          let int = (1. - Math.pow(ds / this.constants.rayDist, 1.5));
          let light = Math.min(1., Math.max(dot*dot, 0.)) * int;

          outClr[2] = int + light;
          outClr[1] = light;
          outClr[0] = light;

          if (ds < (this.constants.rayDist - 0.001)) {
            ray0[0] += rayDir[0] * ds;
            ray0[1] += rayDir[1] * ds;
            ray0[2] += rayDir[2] * ds;
            let ds = 0.00;
            let done = 0.0;
            let nrayDir = [-norm[0], -norm[1], -norm[2]];
            nrayDir = reflectWrap(nrayDir, rayDir);
            norm = [0., 0., 0.];
            ray0[0] += nrayDir[0] * 0.01;
            ray0[1] += nrayDir[1] * 0.01;
            ray0[2] += nrayDir[2] * 0.01;
            for (let i=0; i<this.constants.ICOUNTR; i++) {
              if (done < 0.5) {
                let rnow = [
                  ray0[0] + nrayDir[0] * ds,
                  ray0[1] + nrayDir[1] * ds,
                  ray0[2] + nrayDir[2] * ds
                ];

                norm[0] = 0.; norm[1] = 0.; norm[2] = 0.;

                let m = 0.0;
                let fq = 0.0;
                let nt = 0.0;
                let dmin = 1000.0;
                for (let j=0; j<this.constants.particleCount; j++) {
                  let off = j * 3;
                  let dx  = positions[off], dy  = positions[off+1.], dz  = positions[off+2.];
                  dx -= rnow[0];        dy -= rnow[1];           dz -= rnow[2];
                  let r = Math.sqrt(dx*dx + dy*dy + dz*dz);
                  let x = r / this.constants.rfScale;
                  if (x < 1.0) {
                    let q = 1.0 - x*x*x*(x*(x*6.0-15.0)+10.0);
                    norm[0] += (dx/r) * q;
                    norm[1] += (dy/r) * q;
                    norm[2] += (dz/r) * q;
                    fq += q;
                    nt += q;
                    m += 1.0;
                  } else {
                    dmin = Math.min(dmin, r - this.constants.rfScale);
                  }
                }
                let dist = dmin + 0.1;

                norm[0] /= nt; norm[1] /= nt; norm[2] /= nt;

                if (m > 0.5) {
                  dist = (0.5333 * this.constants.rfScale) * (0.5 - fq);
                }

                ds += dist;
                if (dist < 0.001) {
                  done = 1.0;
                }
                if (ds >= this.constants.rayDistRef) {
                  done = 1.0;
                }
              }
            }
            ds = Math.min(ds, this.constants.rayDistRef);

            if (ds < (this.constants.rayDistRef - 0.001)) {
              let ref = refractWrap(norm, nrayDir, 1./1.5);
              let dot = ref[0] * nrayDir[0] + ref[1] * nrayDir[1] + ref[2] * nrayDir[2];
              let int = 1. - Math.pow(Math.abs(ds) / this.constants.rayDistRef, 1.5);
              let light = Math.min(1., Math.max(dot*dot, 0.)) * int;
              outClr[2] += (int + light) * 0.1;
              outClr[1] += (light) * 0.1;
              outClr[0] += (light) * 0.1;
            }
          }

          this.color(Math.min(outClr[0], 1.), Math.min(outClr[1], 1.), Math.min(outClr[2], 1.), 1.);
        }, {
          graphical: true,
          constants: {
            rayCount: this.rayCount,
            rayDist: this.rayDist,
            rayDistRef: Math.floor(this.rayDist / 4),
            rfScale: this.rfScale,
            bound: this.bound,
            renderSize: this.renderSize,
            particleCount: this.particleCount,
            ICOUNT: 48,
            ICOUNTR: 24
          },
          loopMaxIterations: 48 * this.particleCount,
          output: [this.renderSize, this.renderSize],
          tactic: 'speed',
        })
      ];

      /*
          INIT KERNEL
       */
      const seedPositions = gpu.createKernel(function(){
        var t = random(Math.floor(this.thread.x / 3), this.constants.seed + 0.5);
        var r = t * this.constants.maxr;
        var a = 3.141592 * 2.0 * random(Math.floor(this.thread.x / 3), this.constants.seed + 1.5);
        var comp = this.thread.x % 3;
        if (comp < 0.01) {
          return Math.cos(a) * r;
        } else if (comp < 1.01) {
          return Math.sin(a) * r;
        } else {
          return 0.0;
        }
      }, {
        constants: {
          maxr: 25,
          seed: Math.random() * 1e6,
          particleCount: this.particleCount
        },
        pipeline: true,
        output: [this.particleCount * 3],
        tactic: 'speed',
      });

      const seedVelocities = gpu.createKernel(function(){
        var t = random(this.thread.x, this.constants.seed + 0.5);
        return (t - 0.5) * this.constants.iv;
      }, {
        constants: {
          iv: 1.5,
          seed: Math.random() * 1e6,
          particleCount: this.particleCount,
        },
        pipeline: true,
        output: [this.particleCount * 3],
        tactic: 'speed',
      });

      this.sDensity = [0.125, 0.25, 0.5, 1.0, 1.5, 2.0];
      this.sDensityI = 5;
      this.sMass = [0.05, 0.1, 0.2, 0.4, 0.8];
      this.sMassI = 2;
      this.sIncomp = [1.0, 0.8, 0.6, 0.4, 0.2];
      this.sIncompI = 3;
      this.sVisc = [0.05, 0.1, 0.25, 0.35, 0.5, 0.75, 0.95];
      this.sViscI = 0;

      const seedAttrs = gpu.createKernel(function(){
        var comp = this.thread.x % 6;
        if (comp < 0.01) {
          return 0.25; // radius
        } else if (comp < 1.01) {
          return 0.5; // density
        } else if (comp < 2.01) {
          return 0.2; // mass
        } else if (comp < 3.01) {
          return 0.8; // incompress
        } else if (comp < 4.01) {
          return 0.35; // visc
        } else {
          // type
          if (Math.floor(this.thread.x / 6) > (this.constants.particleCount-33)) {
            return 1.0;
          } else {
            return 0.0;
          }
        }
      }, {
        constants: {
          iv: 50,
          seed: Math.random() * 1e6,
          particleCount: this.particleCount
        },
        pipeline: true,
        output: [this.particleCount * 6],
        tactic: 'speed',
      });

      /*
       * Seed Particles
       */
      this.reset = () => {
        this.cam = {
          p: [0, 0, 17.5],
          dir: [0, 0, -1],
          up: [0, 1, 0],
          nearW: 0.0001,
          farW: 1000,
          farDist: 1000
        };

        this.move = {
          toLR: 0,
          toUD: 0,
          toFR: 0,
          tLR: 0,
          tUD: 0,
          tFR: 0,
          toPull: 0,
          tPull: 0,
          toPullR: 0,
          tPullR: 0
        };

        this.data = {
          pos: seedPositions(),
          vel: seedVelocities(),
          attr: seedAttrs()
        };
      };

      this.reset();

      this.hash = [];
      for (let i=0; i<this.hashLength; i++) {
        for (let j=0; j<this.hashBinLen; j++) {
          this.hash.push(0); this.hash.push(0); this.hash.push(0); this.hash.push(-1);
        }
      }

      this.normv = gpu.createKernel(function(a) {
        const length = Math.sqrt(Math.abs(a[0]*a[0]) + Math.abs(a[1]*a[1]) + Math.abs(a[2]*a[2]));
        return a[this.thread.x] / length;
      }, {
        output: [3],
        pipeline: true,
        tactic: 'speed',
      });

      this.crossv = gpu.createKernel(function(a, b) {
        const a1 = a[0], a2 = a[1], a3 = a[2];
        const b1 = b[0], b2 = b[1], b3 = b[2];
        if (this.thread.x === 0) {
          return a2 * b3 - a3 * b2;
        } else if (this.thread.x === 1) {
          return a3 * b1 - a1 * b3;
        } else {
          return a1 * b2 - a2 * b1;
        }
      }, {
        output: [3],
        pipeline: true,
        tactic: 'speed',
      });
    }

    XSSPS.prototype.hashFn = function(x, y, z) {
      const max2 = Math.floor(this.bound * 2);
      const ix = (Math.floor(x/this.hashWSize)) + max2,
        iy = (Math.floor(y/this.hashWSize)) + max2,
        iz = (Math.floor(z/this.hashWSize)) + max2;
      return ((ix * 137) + (iy * 197) + (iz * 167)) % this.hashLength;
    };

    XSSPS.prototype.updateRender = function(keys, dt) {
      this.dt = dt;
      this.handleInput(keys);
      this.sDensityI = this.sDensityI % this.sDensity.length;
      this.sViscI = this.sViscI % this.sVisc.length;
      this.sMassI = this.sMassI % this.sMass.length;
      this.sIncompI = this.sIncompI % this.sIncomp.length;

      const SUBSTEPS = 2;

      for (let i=0; i<SUBSTEPS; i++) {
        // Update velocities via gravity & pressure
        const prevVel = this.data.vel;
        this.data.vel = this.updateVelocity(
          this.data.pos,
          this.data.vel,
          this.data.attr,
          this.move.tPull,
          this.cam.p,
          this.cam.dir,
          this.move.tPullR,
          this.sDensity[this.sDensityI],
          this.sVisc[this.sViscI],
          this.sMass[this.sMassI],
          this.sIncomp[this.sIncompI],
          this.dt / SUBSTEPS
        );
        deleteTexture(prevVel);

        // Update positions
        const prevPos = this.data.pos;
        this.data.pos = this.updatePosition(
          this.data.pos,
          this.data.vel,
          this.dt / SUBSTEPS
        );
        deleteTexture(prevPos);
      }

      // Render
      this.renderMode = this.renderMode % this.renderKernel.length;
      this.renderKernel[this.renderMode](
        this.data.pos,
        this.cam.p,
        this.cam.dir,
        this.cam.up,
        this.cam.nearW, this.cam.farW, this.cam.farDist
      );
    };

    XSSPS.prototype.handleInput = function(keys) {
      this.move.toLR = this.move.toFR = this.move.toUD = 0;

      if (keys[37]) {
        this.move.toLR -= 1;
      }
      if (keys[39]) {
        this.move.toLR += 1;
      }
      if (keys[38]) {
        this.move.toUD -= 1;
      }
      if (keys[40]) {
        this.move.toUD += 1;
      }
      if (keys[83]) {
        this.move.toFR -= 1;
      }
      if (keys[87]) {
        this.move.toFR += 1;
      }

      if (this.keys[82] && !keys[82]) {
        this.renderMode += 1;
      }
      if (this.keys[27] && !keys[27]) {
        this.reset();
      }
      if (this.keys[49] && !keys[49]) {
        this.sDensityI += 1;
      }
      if (this.keys[50] && !keys[50]) {
        this.sViscI += 1;
      }
      if (this.keys[51] && !keys[51]) {
        this.sMassI += 1;
      }
      if (this.keys[52] && !keys[52]) {
        this.sIncompI += 1;
      }

      if (this.keys[32] && !keys[32]) {
        const prevPos = this.data.pos;
        this.data.pos = this.setPosition(
          this.data.pos,
          (this.particleCount-1) - this.shootIndex,
          this.cam.p,
          this.cam.dir,
          0.5
        );
        deleteTexture(prevPos);
        const prevVel = this.data.vel;
        this.data.vel = this.setVelocity(
          this.data.vel,
          (this.particleCount-1) - this.shootIndex,
          this.cam.dir,
          15
        );
        deleteTexture(prevVel);
        this.shootIndex = (this.shootIndex + 1) % 32;
      }
      if (keys[69]) {
        this.move.toPull = 10;
        this.move.toPullR = 5;
      }
      else {
        this.move.toPull = -1;
        this.move.toPullR = 0.25;
      }

      this.keys = {...keys};

      this.move.tLR += (this.move.toLR - this.move.tLR) * this.dt * 1.5;
      this.move.tUD += (this.move.toUD - this.move.tUD) * this.dt * 1.5;
      this.move.tFR += (this.move.toFR - this.move.tFR) * this.dt * 1.5;
      this.move.tPull += (this.move.toPull - this.move.tPull) * this.dt * 1.5;
      this.move.tPullR += (this.move.toPullR - this.move.tPullR) * this.dt * 1.5;
      this.updateCamera();
      if (this.onAfterInput) this.onAfterInput();
    };

    XSSPS.prototype.updateCamera = function() {
      const prevUp = this.cam.up;
      const crossv1 = this.crossv(
        this.cam.dir,
        this.cam.up
      );
      const crossv2 = this.crossv(
        crossv1,
        this.cam.dir
      );
      deleteTexture(this.cam.up);
      this.cam.up = this.normv(crossv2);
      deleteTexture(crossv1);
      deleteTexture(crossv2);
      deleteTexture(prevUp);
      const prevDir = this.cam.dir;
      this.cam.dir = this.rotateCamera(
        this.move.tLR / 35,
        -this.move.tUD / 35,
        this.cam.p,
        this.cam.dir,
        this.cam.up,
        1, 3.5, 2
      );
      deleteTexture(prevDir);
      const prevP = this.cam.p;
      this.cam.p = this.updateCameraPosition(
        this.cam.dir,
        this.cam.p,
        this.move.tFR,
        this.dt
      );
      deleteTexture(prevP);
    };
  </script>
  <script>
    function SSPS(psim) {
      this.dt = 0;
      this.psim = psim;
      this.keys = {};
      this.controls = {};
      this.buildControls();
    }

    SSPS.prototype.buildControls = function() {
      let html = `<div>
      SSPS by Chadams - <span id="fps"></span> fps - <span id="particle-count"></span> particles @ <span id="size"></span><br />
      [W] - Forward, [S] - Reverse, [ARROWS] - Look<br />
      [E] - Grab, [SPACE] - Shoot Particle<br />
      [R] - Cycle Render Mode: <span id="render-mode"></span><br />
      [1] - Cycle Particle Density: <span id="particle-density"></span><br />
      [2] - Cycle Particle Viscosity: <span id="particle-viscosity"></span><br />
      [3] - Cycle Particle Mass: <span id="particle-mass"></span><br />
      [4] - Cycle Particle Incompressiveness: <span id="particle-incompressiveness"></span><br />
      [ESC] - Reset Particles & Camera<br />
      [P] - Pause<br />
      </div>`;
      const renderer = document.createElement('div');
      renderer.innerHTML = html;
      html = this.controls.html = renderer.children[0];

      this.controls.fps = html.querySelector('#fps');
      this.controls.particleCount = html.querySelector('#particle-count');
      this.controls.particleCount.innerHTML = this.psim.particleCount;
      this.controls.size = html.querySelector('#size');
      this.controls.size.innerHTML = this.psim.renderSize + 'x' + this.psim.renderSize;
      this.controls.renderMode = html.querySelector('#render-mode');
      this.controls.particleDensity = html.querySelector('#particle-density');
      this.controls.particleViscosity = html.querySelector('#particle-viscosity');
      this.controls.particleMass = html.querySelector('#particle-mass');
      this.controls.particleIncompressiveness = html.querySelector('#particle-incompressiveness');
      this.updateControls();
    };

    SSPS.prototype.mkOpt = function(lst, i) {
      i = i % lst.length;
      let ret = '';
      for (let j=0; j<lst.length; j++) {
        if (j === i) {
          ret += '>>' + lst[j] + '<< ';
        }
        else {
          ret += ' ' + lst[j] + ' ';
        }
      }
      return ret;
    };

    SSPS.prototype.updateFPS = function() {
      this.controls.fps.innerHTML = Math.round(1 / this.dt);
    };

    SSPS.prototype.updateControls = function() {
      this.controls.renderMode.innerHTML = this.mkOpt([1, 2], this.psim.renderMode);
      this.controls.particleDensity.innerHTML = this.mkOpt(this.psim.sDensity, this.psim.sDensityI);
      this.controls.particleViscosity.innerHTML = this.mkOpt(this.psim.sVisc, this.psim.sViscI);
      this.controls.particleMass.innerHTML = this.mkOpt(this.psim.sMass, this.psim.sMassI);
      this.controls.particleIncompressiveness.innerHTML = this.mkOpt(this.psim.sIncomp, this.psim.sIncompI);
    };

    SSPS.prototype.updateRender = function() {
      this.psim.updateRender(this.keys, this.dt);
      this.updateFPS();
    };

    SSPS.prototype.start = function(tick) {
      if (this.running) return;
      document.title = 'SSPS - Chadams Studios (slightly upgraded)';
      this.running = true;
      requestAnimationFrame(tick);
    };

    SSPS.prototype.init = function () {
      let lTime = Date.timeStamp();
      let lDt = 1/60;
      this.time = 0;

      document.body.addEventListener('keydown', (e) => {
        e = e || window.event;
        this.keys[e.keyCode] = true;
      });

      document.body.addEventListener('keyup', (e) => {
        e = e || window.event;
        if (e.keyCode === 80) {
          if (this.running) {
            this.stop();
          } else {
            this.start(tick);
          }
          return;
        }
        this.keys[e.keyCode] = false;
      });

      const tick = () => {
        if (!this.running) {
          return;
        }

        const cTime = Date.timeStamp();
        const dt = (Math.max(Math.min(cTime - lTime, 1/10), 1/240) || (1/60)) * 0.1 + lDt * 0.9;
        this.dt = dt;
        lDt = dt;
        lTime = cTime;

        this.time += dt;

        this.updateRender();

        requestAnimationFrame(tick);
      };
      this.start(tick);
    };

    SSPS.prototype.stop = function () {
      this.running = false;

      document.title = 'SSPS Stopped - Chadams Studios (slightly upgraded)';
    };

    Date.timeStamp = function() {
      return new Date().getTime() / 1000.0;
    };

    function deleteTexture(texture) {
      if (texture && texture.delete) {
        texture.delete();
      }
    }
  </script>
</head>
<body style="background-color: black; padding: 0; margin: 0;"></body>
<script>
  const psim = new XSSPS(64, 256, () => {
    inst.updateControls();
  });
  const inst = new SSPS(psim);
  const { html } = inst.controls;
  html.style.color = '#78788b';
  html.style.position = 'absolute';
  document.body.appendChild(html);
  psim.canvas.style.margin = '0 auto';
  psim.canvas.style.display = 'block';
  psim.canvas.style.width = '100vh';
  psim.canvas.style.padding = '0';
  document.body.appendChild(psim.canvas);
  inst.init();
</script>
</html>
